Pro Entity Framework Core 2 for ASP.NET Core MVC 翻译

第 6 章 运动商店:更新和删除数据

作者:Adam Freeman
翻译:陈广
日期:2018-12-15


目前,SportsStore 应用程序可以将Product对象存储在数据库中,并执行查询以重新读取它们。大多数应用程序还需要在数据存储后对其进行更改的能力,包括完全删除对象。在本章中,我添加了对更新和删除Product对象的支持。我还描述了在您自己的项目中添加这些特性进所遇到的问题,并解释如何解决它们。

准备本章

本章,我将继续使用第四章创建的 SportsStore 项目以及我在第5章向其添加的 Entity Framework Core。在 SportsStore 项目文件夹中运行清单6-1所示的命令以删除和重建数据库,这也确保了您从项目中获得预期结果。

提示:您可以在本书的 GitHub 存储库中下载本章所需的 SportsStore 项目,或者每一章的项目:https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc

清单 6-1:删除和重建数据库

dotnet ef database drop --force
dotnet ef database update

使用dotnet run命令启动应用程序并导航至 http://localhost:5000;您将看到如图6-1所示的内容。

图6-1 运行示例应用程序

使用表6-1中的数据值填充 HTML 表单,它们是为本章中的示例提供的数据。

表 6-1:创建测试产品对象的值

Name Category Purchase Price Retail Price
Kayak Watersports 200 275
Lifejacket Watersports 30 48.95
Soccer Ball Soccer 17 19.50

当您添加完三个产品的详细信息后,将看到如图6-2所示的结果。

图6-2 添加测试数据

更新对象

Entity Framework Core 支持许多更新对象的不同方法,我将在12和21章进行描述。本章,我从最简单的技术开始,使用 MVC 模型绑定创建的对象来完全替换存储在数据库中的对象。

更新存储库

首先,我已经更改了IRepository接口,以添加应用程序的其它部分可以用来检索和更新现有对象的方法,如清单6-2所示。

清单 6-2:Models 文件夹下的 IRepository.cs 文件,添加方法

using System.Collections.Generic;

namespace SportsStore.Models
{
    public interface IRepository
    {
        IEnumerable<Product> Products { get; }
        Product GetProduct(long key);
        void AddProduct(Product product);
        void UpdateProduct(Product product);
    }
}

GetProduct方法将使用一个主键值提供单个Product对象。UpdateProduct方法接收一个Product对象且不返回任何结果。在清单6-3中,我已经向存储库实现类添加了新的方法。

清单 6-3:Models 文件夹下的 DataRepository.cs 文件,添加方法

using System.Collections.Generic;
using System.Linq;

namespace SportsStore.Models
{
    public class DataRepository : IRepository
    {
        private DataContext context;
        public DataRepository(DataContext ctx) => context = ctx;
        public IEnumerable<Product> Products => context.Products.ToArray();
        public Product GetProduct(long key) => context.Products.Find(key);

        public void AddProduct(Product product)
        {
            this.context.Products.Add(product);
            this.context.SaveChanges();
        }

        public void UpdateProduct(Product product)
        {
            context.Products.Update(product);
            context.SaveChanges();
        }
    }
}

数据库 context 的Products属性返回的DbSet<Product>提供了实现新方法所需的功能。Find方法接收一个主键值,并查询数据库获取它所对应的对象。Update方法接收一个Product对象,并使用它来更新数据库,以替换数据库中具有相同主键的对象。与所有更改数据库的操作一样,我必须在Update方法之后调用SaveChanges方法。

提示:要记住调用SaveChanges方法可能有些尴尬,但它很快就变成了第二种性质,这种方法意味着您可以通过调用 context 对象的方法来设置多个更改,然后通过调用单个SaveChanges将它们同时发送到数据库中。我在第24章详细解释了这一点。

更新控制器并创建视图

下一步是更新 Home 控制器,以便有 action 方法允许用户选择要编辑的Product对象并将更改发送到应用程序,如清单6-4所示。我还注释掉了清除控制台的语句,这样 Entity Framework Core 为新方法执行的 SQL 查询就更容易看到了。

清单 6-4:Controllers 文件夹下的 HomeController.cs 文件,添加 Actions

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo) => repository = repo;

        public IActionResult Index()
        {
            //System.Console.Clear();
            return View(repository.Products);
        }

        [HttpPost]
        public IActionResult AddProduct(Product product)
        {
            repository.AddProduct(product);
            return RedirectToAction(nameof(Index));
        }

        public IActionResult UpdateProduct(long key)
        {
            return View(repository.GetProduct(key));
        }

        [HttpPost]
        public IActionResult UpdateProduct(Product product)
        {
            repository.UpdateProduct(product);
            return RedirectToAction(nameof(Index));
        }
    }
}

您可以看到 action 方法如何映射到存储库提供的功能上,并一直映射到数据库 context 类。为了给控制器的新 action 提供一个视图,我在 Views/Home 文件夹下添加了一个名为 UpdateProduct.cshtml 的文件,其内容如清单6-5所示。

清单 6-5:Views/Home 文件夹下的 UpdateProduct.cshtml 文件的内容

@model Product

<h3 class="p-2 bg-primary text-white text-center">Update Product</h3>

<form asp-action="UpdateProduct" method="post">
    <div class="form-group">
        <label asp-for="Id"></label>
        <input asp-for="Id" class="form-control" readonly />
    </div>
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="Category"></label>
        <input asp-for="Category" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="PurchasePrice"></label>
        <input asp-for="PurchasePrice" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="RetailPrice"></label>
        <input asp-for="RetailPrice" class="form-control" />
    </div>
    <div class="text-center">
        <button class="btn btn-primary" type="submit">Save</button>
        <a asp-action="Index" class="btn btn-secondary">Cancel</a>
    </div>
</form>

该视图向用户提供一个 HTML 表单,可用于更改Product对象的属性,但Id属性除外,Id属性用作主键。一旦分配了主键,就不能轻易地更改它们,如果需要不同的键值,那么删除对象并创建新的对象是更为简单的方法。因此,我为显示Id属性值的input元素添加了readonly属性,以便让它无法更改。

为加入更新功能,我为每个Index视图中显示的Product对象添加了一个button元素,如清单6-6所示。我还向网格中添加了一列以显示Id属性。

清单 6-6:Views/Home 文件夹下的 Index.cshtml 文件,加入更新

@model IEnumerable<Product>

<h3 class="p-2 bg-primary text-white text-center">Products</h3>

<div class="container-fluid mt-3">
    <div class="row">
        <div class="col-1 font-weight-bold">Id</div>
        <div class="col font-weight-bold">Name</div>
        <div class="col font-weight-bold">Category</div>
        <div class="col font-weight-bold text-right">Purchase Price</div>
        <div class="col font-weight-bold text-right">Retail Price</div>
        <div class="col"></div>
    </div>
    <form asp-action="AddProduct" method="post">
        <div class="row p-2">
            <div class="col-1"></div>
            <div class="col"><input name="Name" class="form-control" /></div>
            <div class="col"><input name="Category" class="form-control" /></div>
            <div class="col">
                <input name="PurchasePrice" class="form-control" />
            </div>
            <div class="col">
                <input name="RetailPrice" class="form-control" />
            </div>
            <div class="col">
                <button type="submit" class="btn btn-primary">Add</button>
            </div>
        </div>
    </form>
    <div>
        @if (Model.Count() == 0)
        {
            <div class="row">
                <div class="col text-center p-2">No Data</div>
            </div>
        }
        else
        {
            @foreach (Product p in Model)
            {
                <div class="row p-2">
                    <div class="col-1">@p.Id</div>
                    <div class="col">@p.Name</div>
                    <div class="col">@p.Category</div>
                    <div class="col text-right">@p.PurchasePrice</div>
                    <div class="col text-right">@p.RetailPrice</div>
                    <div class="col">
                        <a asp-action="UpdateProduct" asp-route-key="@p.Id"
                           class="btn btn-outline-primary">
                            Edit
                        </a>
                    </div>
                </div>
            }
        }
    </div>
</div>

使用dotnet run启动应用程序,并导航至 http://localhost:5000; 您将看到新元素,它们显示了主键及为每个产品提供一个【Edit】按钮,如图6-3所示。

图6-3 向 Index 视图添加元素

单击【Soccer Ball】产品的【Edit】按钮,更改【Purchase Price】字段值为 16.5,并单击【Save】按钮。浏览器将向 Home 控制器的UpdateProduct action 方法发送表单数据,action 方法将接收由 MVC 模型绑定器创建的Product对象。Product对象将传递给数据库 context 类的Update方法,当调用SaveChanges方法时,表单数据值将存储在数据库中,如图6-4所示。

图6-4 更新对象

如果您检查应用程序生成的日志消息,将看到所执行的 action 所生成的将发送到数据库服务器的 SQL 命令。当您单击【Edit】按钮,Entity Framework Core 将使用以下命令查询数据库以获取 Soccer Ball 的详细信息:

SELECT TOP(1) [e].[Id], [e].[Category], [e].[Name], [e].[PurchasePrice],
    [e].[RetailPrice]
FROM [Products] AS [e]
WHERE [e].[Id] = @__get_Item_0

我在清单6-6中所使用的Find方法被转换为SELECT命令以查找单个对象,并使用了TOP关键字。当您单击【Save】按钮,Entity Framework Core 使用以下命令更新数据库:

UPDATE [Products] SET [Category] = @p0, [Name] = @p1, [PurchasePrice] = @p2,
    [RetailPrice] = @p3
WHERE [Id] = @p4;

Update方法被转换为 SQL 的UPDATE命令,并存储从 HTTP 请求所接收到的表单值。

只更新被改变的属性

执行更新的基本构建块已经就位,但结果效率低下,因为 Entity Framework Core 没有基线来找出已更改的内容,因此别无选择,只能存储所有属性。若要查看问题,请单击其中一个产品的【Edit】按钮,然后单击【Save】而不做任何更改。尽管没有新的数据值,但应用程序生成的日志消息显示,Entity Framework Core 生成的UPDATE命令为Product类定义的所有属性发送了值。

UPDATE [Products] SET [Category] = @p0, [Name] = @p1, [PurchasePrice] = @p2,
    [RetailPrice] = @p3
WHERE [Id] = @p4;

Entity Framework Core 包含一个更改检测功能,它可以计算出哪些属性已经更改。对于Product这样简单的数据模型,可能不是什么问题,但对于更为复杂的数据模型,检测更改非常重要。

更改检测功能需要一个基线来比较从用户收到的数据。如我在12章所描述的,可以使用不同的方法提供基线,但本章我将使用最简单的方法,即查询数据库中的现有数据。在清单6-7中,我已经更新了存储库实现类,以便它查询数据库中存储的Product对象,并使用它来避免更新未更改的属性。

提示:查询的成本必须与避免不必要的更新的好处相平衡,但这种方法简单可靠,并且与 Entity Framework Core 提供的功能很好地配合,以阻止两个用户试图更新相同的数据,如第20章所述。

清单 6-7:Models 文件夹下的 DataRepository.cs 文件,避免不必要的更新

using System.Collections.Generic;
using System.Linq;

namespace SportsStore.Models
{
    public class DataRepository : IRepository
    {
        //private List<Product> data = new List<Product>();
        private DataContext context;
        public DataRepository(DataContext ctx) => context = ctx;
        public IEnumerable<Product> Products => context.Products.ToArray();
        public Product GetProduct(long key) => context.Products.Find(key);

        public void AddProduct(Product product)
        {
            this.context.Products.Add(product);
            this.context.SaveChanges();
        }

        public void UpdateProduct(Product product)
        {
            Product p = GetProduct(product.Id);
            p.Name = product.Name;
            p.Category = product.Category;
            p.PurchasePrice = product.PurchasePrice;
            p.RetailPrice = product.RetailPrice;
            // context.Products.Update(product);
            context.SaveChanges();
        }
    }
}

此代码将应用程序中的两个不同特性连接起来。Entity Framework Core 对它从查询数据创建的对象执行更改跟踪,而 MVC 模型绑定器则从 HTTP 数据创建对象。这两个对象的来源并不是集成的,如果不注意保持它们的分离,就会出现问题。利用更改跟踪的最安全方法是查询数据库,然后复制 HTTP 数据中的值,正如我在清单中所做的那样。当调用SaveChanges方法时,Entity Framework Core 将计算出哪些值被更改,并只更新数据库中的那些属性。

提示:注意我已经注释了Update方法的调用,当一个查询提供基线数据时,不需要它。

要查看它是如何工作的,使用dotnet run启动应用程序,导航至 http://localhost:5000,并单击【Kayak】新产品的【Edit】按钮。更改【Retail Price】值为 300,并单击【Save】按钮。检查由应用程序生成的日志消息,您将看到 Entity Framework Core 发送到数据库的UPDATE命令仅更新了已经更改的属性。

UPDATE [Products] SET [RetailPrice] = @p0
WHERE [Id] = @p1;

执行批量更新

在需要使用单个操作更改多个对象的专用管理角色的应用程序中,通常需要进行批量更新。更新的确切性质将有所不同,但大容量更新的常见原因包括纠正数据输入错误或将对象重新分配到新类别,这两种方法对单个对象执行起来都很费时。使用 Entity Framework Core 很容易执行批量更新,但需要付出一点努力才能让它们与应用程序的 ASP.NET Core MVC 部分顺利工作。

更新视图与控制器

为添加对批量更新的支持,我更新了 Index 视图以包含一个【Edit All】按钮,并让其指向UpdateAll action。我还添加了一个名为UpdateAllViewBag属性,当它的值为true时,将显示一个名为 InlineEditor.cshtml 的分部视图,如清单6-8所示。

清单 6-8:Views/Home 文件夹下的 Index.cshtml 文件,支持批量更新

@model IEnumerable<Product>

<h3 class="p-2 bg-primary text-white text-center">Products</h3>

<div class="container-fluid mt-3">
    @if (ViewBag.UpdateAll != true)
    {
        <div class="row">
            <div class="col-1 font-weight-bold">Id</div>
            <div class="col font-weight-bold">Name</div>
            <div class="col font-weight-bold">Category</div>
            <div class="col font-weight-bold text-right">Purchase Price</div>
            <div class="col font-weight-bold text-right">Retail Price</div>
            <div class="col"></div>
        </div>
        <form asp-action="AddProduct" method="post">
            <div class="row p-2">
                <div class="col-1"></div>
                <div class="col"><input name="Name" class="form-control" /></div>
                <div class="col"><input name="Category" class="form-control" /></div>
                <div class="col">
                    <input name="PurchasePrice" class="form-control" />
                </div>
                <div class="col">
                    <input name="RetailPrice" class="form-control" />
                </div>
                <div class="col">
                    <button type="submit" class="btn btn-primary">Add</button>
                </div>
            </div>
        </form>
        <div>
            @if (Model.Count() == 0)
            {
                <div class="row">
                    <div class="col text-center p-2">No Data</div>
                </div>
            }
            else
            {
                @foreach (Product p in Model)
                {
                    <div class="row p-2">
                        <div class="col-1">@p.Id</div>
                        <div class="col">@p.Name</div>
                        <div class="col">@p.Category</div>
                        <div class="col text-right">@p.PurchasePrice</div>
                        <div class="col text-right">@p.RetailPrice</div>
                        <div class="col">
                            <a asp-action="UpdateProduct" asp-route-key="@p.Id"
                               class="btn btn-outline-primary">
                                Edit
                            </a>
                        </div>
                    </div>
                }
            }
        </div>
        <div class="text-center">
            <a asp-action="UpdateAll" class="btn btn-primary">Edit All</a>
        </div>
    }
    else
    {
        @Html.Partial("InlineEditor", Model)
    }
</div>

我在 Views/Home 文件夹下添加了一个名为 InlineEditor.cshtml 的文件以创建分部视图,内容如清单6-9所示。

清单 6-9:Views/Home 文件夹下的 InlineEditor.cshtml 文件的内容

@model IEnumerable<Product>
<div class="row">
    <div class="col-1 font-weight-bold">Id</div>
    <div class="col font-weight-bold">Name</div>
    <div class="col font-weight-bold">Category</div>
    <div class="col font-weight-bold">Purchase Price</div>
    <div class="col font-weight-bold">Retail Price</div>
</div>
@{ int i = 0; }
<form asp-action="UpdateAll" method="post">
    @foreach (Product p in Model)
    {
        <div class="row p-2">
            <div class="col-1">
                @p.Id
                <input type="hidden" name="Products[@i].Id" value="@p.Id" />
            </div>
            <div class="col">
                <input class="form-control" name="Products[@i].Name"
                       value="@p.Name" />
            </div>
            <div class="col">
                <input class="form-control" name="Products[@i].Category"
                       value="@p.Category" />
            </div>
            <div class="col text-right">
                <input class="form-control" name="Products[@i].PurchasePrice"
                       value="@p.PurchasePrice" />
            </div>
            <div class="col text-right">
                <input class="form-control" name="Products[@i].RetailPrice"
                       value="@p.RetailPrice" />
            </div>
        </div>
        i++;
    }
    <div class="text-center m-2">
        <button type="submit" class="btn btn-primary">Save All</button>
        <a asp-action="Index" class="btn btn-outline-primary">Cancel</a>
    </div>
</form>

分部视图为对象集合创建了一组表单元素,其名称遵循 MVC 约定,因此Id属性被赋予名称Products[0].IdProducts[1].Id等等。设置input元素的名称需要一个计数器,它会产生 Razor 和 C# 表达式的尴尬组合。

在清单6-10中,我向 Home 控制器中添加了 action 方法,它将允许用户启动批量编辑进程并提交数据。

清单 6-10:Controllers 文件夹下的 HomeController.cs 文件,添加 Action 方法

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo) => repository = repo;

        public IActionResult Index()
        {
            //System.Console.Clear();
            return View(repository.Products);
        }

        [HttpPost]
        public IActionResult AddProduct(Product product)
        {
            repository.AddProduct(product);
            return RedirectToAction(nameof(Index));
        }

        public IActionResult UpdateProduct(long key)
        {
            return View(repository.GetProduct(key));
        }

        [HttpPost]
        public IActionResult UpdateProduct(Product product)
        {
            repository.UpdateProduct(product);
            return RedirectToAction(nameof(Index));
        }

        public IActionResult UpdateAll()
        {
            ViewBag.UpdateAll = true;
            return View(nameof(Index), repository.Products);
        }

        [HttpPost]
        public IActionResult UpdateAll(Product[] products)
        {
            repository.UpdateAll(products);
            return RedirectToAction(nameof(Index));
        }
    }
}

POST版本的UpdateAll接收一个Product对象数组,它是 MVC 模型绑定器将从表单数据创建的,并传递给同名的存储库方法。

更新存储库

在清单6-11中,我为存储库接口添加了一个新的执行批量更新的方法。

清单 6-11:Models 文件夹下的 IRepository.cs 文件,添加方法

using System.Collections.Generic;

namespace SportsStore.Models
{
    public interface IRepository
    {
        IEnumerable<Product> Products { get; }
        Product GetProduct(long key);
        void AddProduct(Product product);
        void UpdateProduct(Product product);
        void UpdateAll(Product[] products);
    }
}

为完成功能,我向存储库实现添加了UpdateAll方法,以使用从 HTTP 请求接收到的数据来更新数据库,如清单6-12所示。

清单 6-12:Models 文件夹下的 DataRepository.cs 文件,执行批量编辑

using System.Collections.Generic;
using System.Linq;

namespace SportsStore.Models
{
    public class DataRepository : IRepository
    {
        //private List<Product> data = new List<Product>();
        private DataContext context;
        public DataRepository(DataContext ctx) => context = ctx;
        public IEnumerable<Product> Products => context.Products.ToArray();
        public Product GetProduct(long key) => context.Products.Find(key);

        public void AddProduct(Product product)
        {
            this.context.Products.Add(product);
            this.context.SaveChanges();
        }

        public void UpdateProduct(Product product)
        {
            Product p = GetProduct(product.Id);
            p.Name = product.Name;
            p.Category = product.Category;
            p.PurchasePrice = product.PurchasePrice;
            p.RetailPrice = product.RetailPrice;
            // context.Products.Update(product);
            context.SaveChanges();
        }

        public void UpdateAll(Product[] products)
        {
            context.Products.UpdateRange(products);
            context.SaveChanges();
        }
    }
}

DbSet<T>类提供了处理单个对象和对象集合的方法。本例中,我使用了UpdateRange方法,它是Update方法的集合版本。当调用SaveChanges方法时, Entity Framework Core 将发送一系列 SQL UPDATE命令以更新服务器。使用dotnet run启动应用程序,导航至 http://localhost:5000,并单击【Edit All】按钮以显示批量编辑功能,如图6-5所示。

图6-5 编辑多个对象

为批量更新使用更改检测

清单6-12的代码没有使用 Entity Framework Core 的更改检测功能,这意味着所有Product对象的所有属性都将被更新。为仅更新被更改的值,我更改了存储库灰中的UpdateAll方法,如清单6-13所示。

清单 6-13:Models 文件夹下的 DataRepository.cs 文件,使用更改检测

...
public void UpdateAll(Product[] products)
{
    //context.Products.UpdateRange(products);
    Dictionary<long, Product> data = products.ToDictionary(p => p.Id);
    IEnumerable<Product> baseline =
        context.Products.Where(p => data.Keys.Contains(p.Id));
    foreach (Product databaseProduct in baseline)
    {
        Product requestProduct = data[databaseProduct.Id];
        databaseProduct.Name = requestProduct.Name;
        databaseProduct.Category = requestProduct.Category;
        databaseProduct.PurchasePrice = requestProduct.PurchasePrice;
        databaseProduct.RetailPrice = requestProduct.RetailPrice;
    }
    context.SaveChanges();
}
...

执行更新的过程可能有些复杂。首先,使用键的Id属性创建从 MVC 模型绑定器接收的Product对象的字典。我使用键集合来查询数据库中的相应对象,如下所示:

IEnumerable<Product> baseline =
    context.Products.Where(p => data.Keys.Contains(p.Id));

我枚举查询对象并从请求对象复制属性值。当调用SaveChanges方法时,Entity Framework Core 执行更改检测,并只更新已更改的值。使用dotnet run启动应用程序,并导航至 http://localhost:5000,单击【Edit All】按钮。将第一个产品的 Name 字段更改为“Green Kayak”,将 Lifejacket 的Retail Price 字段更改为 50。单击【Save All】按钮并检查由应用程序生成的日志消息。为获取检测更改的基线数据,Entity Framework Core 向数据库发送了以下查询:

SELECT [p].[Id], [p].[Category], [p].[Name], [p].[PurchasePrice], [p].[RetailPrice]
FROM [Products] AS [p]
WHERE [p].[Id] IN (1, 2, 3)

从此数据创建的对象用于检测更改。Entity Framework Core 计算出哪些属性具有新值,并向数据库发送两个UPDATE命令。

...
UPDATE [Products] SET [Name] = @p0
WHERE [Id] = @p1;
...
UPDATE [Products] SET [RetailPrice] = @p2
WHERE [Id] = @p3;
...

您可以看到,Name值由第一个命令更改,RetailPrice值由第二个命令更改,这与使用应用程序的 MVC 部分所做的更改相对应。

删除数据

从数据库删除对象是一个简单的过程,不过,正如我在第7章中解释的那样,随着数据模型的增长,它可以变得更加复杂。在清单6-14中,我向存储库接口添加了Delete方法。

清单 6-14:Models 文件夹下的 IRepository.cs 文件,添加方法

using System.Collections.Generic;

namespace SportsStore.Models
{
    public interface IRepository
    {
        IEnumerable<Product> Products { get; }
        Product GetProduct(long key);
        void AddProduct(Product product);
        void UpdateProduct(Product product);
        void UpdateAll(Product[] products);
        void Delete(Product product);
    }
}

在清单6-15中,我更新了存储库实现类以添加对Delete方法的支持。

清单 6-15:Models 文件夹下的 DataRepository.cs 文件,删除对象

using System.Collections.Generic;
using System.Linq;

namespace SportsStore.Models
{
    public class DataRepository : IRepository
    {
        private DataContext context;
        public DataRepository(DataContext ctx) => context = ctx;
        public IEnumerable<Product> Products => context.Products.ToArray();
        public Product GetProduct(long key) => context.Products.Find(key);
    
        // ...其它省略...

        public void Delete(Product product)
        {
            context.Products.Remove(product);
            context.SaveChanges();
        }
    }
}

DbSet<T>RemoveRemoveRange方法用于从数据库中删除一个或多个对象。与修改数据库的其他操作一样,在调用SaveChanges方法之前,不会删除任何数据。

在运行应用程序时,我向 Home 控制器添加了一个 action 方法,它接收要从 HTTP 请求中删除的Product对象的细节,并将它们传递给存储库,如清单6-16所示。

清单 6-16:Controllers 文件夹下的 HomeController.cs 文件,添加一个 Action 方法

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo) => repository = repo;

        // ...其它 action 方法省略...

        [HttpPost]
        public IActionResult Delete(Product product)
        {
            repository.Delete(product);
            return RedirectToAction(nameof(Index));
        }
    }
}

为了完成这个功能,我为 Home 控制器使用的 Index 视图所显示的每个Product对象添加了一个form元素,以便用户能够触发一个删除,如清单6-17所示。

提示:表单包含现有的【Edit】按钮元素,这样浏览器将并排显示这两个按钮。

清单 6-17:Views/Home 文件夹下的 Index.cshtml 文件,添加表单

@model IEnumerable<Product>

<h3 class="p-2 bg-primary text-white text-center">Products</h3>

<div class="container-fluid mt-3">
    @if (ViewBag.UpdateAll != true)
    {
        <div class="row">
            <div class="col-1 font-weight-bold">Id</div>
            <div class="col font-weight-bold">Name</div>
            <div class="col font-weight-bold">Category</div>
            <div class="col font-weight-bold text-right">Purchase Price</div>
            <div class="col font-weight-bold text-right">Retail Price</div>
            <div class="col"></div>
        </div>
        <form asp-action="AddProduct" method="post">
            <div class="row p-2">
                <div class="col-1"></div>
                <div class="col"><input name="Name" class="form-control" /></div>
                <div class="col"><input name="Category" class="form-control" /></div>
                <div class="col">
                    <input name="PurchasePrice" class="form-control" />
                </div>
                <div class="col">
                    <input name="RetailPrice" class="form-control" />
                </div>
                <div class="col">
                    <button type="submit" class="btn btn-primary">Add</button>
                </div>
            </div>
        </form>
        <div>
            @if (Model.Count() == 0)
            {
                <div class="row">
                    <div class="col text-center p-2">No Data</div>
                </div>
            }
            else
            {
                @foreach (Product p in Model)
                {
                    <div class="row p-2">
                        <div class="col-1">@p.Id</div>
                        <div class="col">@p.Name</div>
                        <div class="col">@p.Category</div>
                        <div class="col text-right">@p.PurchasePrice</div>
                        <div class="col text-right">@p.RetailPrice</div>
                        <div class="col">
                            <form asp-action="Delete" method="post">
                                <a asp-action="UpdateProduct" asp-route-key="@p.Id"
                                   class="btn btn-outline-primary">
                                    Edit
                                </a>
                                <input type="hidden" name="Id" value="@p.Id" />
                                <button type="submit" class="btn btn-outline-danger">
                                    Delete
                                </button>
                            </form>
                        </div>
                    </div>
                }
            }
        </div>
        <div class="text-center">
            <a asp-action="UpdateAll" class="btn btn-primary">Edit All</a>
        </div>
    }
    else
    {
        @Html.Partial("InlineEditor", Model)
    }
</div>

注意,表单只包含用于Id属性的input元素。这就是 Entity Framework Core 用于从数据库中删除对象的全部内容,即使操作是对一个完整的Product对象执行的。我没有发送不被使用的附加数据,而是只发送主键值,MVC 模型绑定器将使用该键值来创建Product对象,保留该类型的所有其他属性为空或默认值。

注意:这是另一个示例,说明了有多少实现细节可以泄漏到应用程序的其余部分。只为删除操作发送Id值是有效和简单的,但它依赖于 Entity Framework Core 是如何工作的知识,这就产生了对数据存储方式的依赖。另一种方法不是依赖 Entity Framework Core 的行为,而是为将被忽略的属性发送值,这将增加应用程序所需的带宽。一些设计决策是明确的,但另一些则需要在不太理想的备选方案之间做出艰难的选择。

要测试删除功能,使用dotnet run启动应用程序,并导航至 http://localhost:5000,并单击【Soccer Ball】荐的【Delete】按钮。Product对象将从数据库中删除,如图6-6所示。

图6-6 从数据库删除一个对象

常见问题与解决方法

用于更新和删除数据的 Entity Framework Core 功能相当简单,尽管在使用这些功能时,使用由来自 HTTP 请求的 MVC 模型绑定器创建的对象可能会遇到困难。在接下来的部分中,我将描述您最可能遇到的问题,并解释如何解决这些问题。

对象没有更新或删除

如果应用程序看起来工作正常,但对象没有被修改,那么首先要检查的是,您是否记得在存储库实现类中调用SaveChanges方法。Entity Framework Core 只会在SaveChanges方法被调用之后更新数据库,如果您忘记了,它将悄然放弃更改。

“Reference Not Set to an Instance of an Object” 异常

此异常是由于试图更新其主要属性设置为null或 0 的对象造成的。造成此问题的最常见原因是忘记在用于更新对象的 HTML 表单中包含主键属性的值。虽然不能更改主键值,但必须确保将值作为 HTML 表单的一部分提供。如果不希望用户看到主键值,请使用隐藏的输入元素。

“Instance of Entity Type Cannot be Tracked” 异常

当您在使用 Entity Framework Core 为同一个对象查询数据库之后,使用 MVC 模型绑定器创建的对象调用 Entity Framework Core Update方法时,会导致此异常。数据库 context 类跟踪为使更改检测工作而创建的对象,当您试图引入由 MVC 框架创建的冲突对象时,Entity Framework Core 无法处理。

只有在没有查询基线数据的情况下,才能使用Update方法。为了避免这个问题,将 MVC 模型绑定创建的对象的属性复制到 Entity Framework Core 创建的对象,如清单6-7所示。

“Property Has a Temporary Value” 异常

当您试图向应用程序发送 HTTP 请求以删除对象,但忘记包含主键属性的值时,会发生此异常。MVC 模型绑定器将创建一个对象,其主键值为属性类型的默认值,用于在存储新对象时等待数据库服务器分配值时指示一个临时值。若要防止此异常,请确保包含在 HTML 表单中提供主键值的input元素。可以将input元素的类型设置为hidden,以防止用户更改值。

更新导致 0 值

如果更新将数字属性设置为零,则可能的原因是 HTML 表单不包含此属性的值,或者用户输入的值不能由 MVC 模型绑定器解析为属性的数据类型。要修复第一个问题,请确保数据模型类定义的所有属性都有值。要解决第二个问题,请使用 MVC 验证功能在无法处理数据值时进行部分更新。

总结

本章我在 SportsStore 应用程序中添加了对更新和删除对象的支持。我演示了如何更新单个对象以及执行批量更新,以及如何为 Entity Framework Core 提供基线数据,以实现其变化检测功能。我还向您展示了如何删除数据,这对于只有一个类的数据模型来说很简单,但随着数据模型的增长,它变得更加复杂。在下一章中,我扩展了用于 SportsStore 应用的数据模型。

;

© 2018 - IOT小分队文章发布系统 v0.3